window.onload = function() {

var particles = [];
var rate = 0;
init();
var lastTime = new Date().getTime();
scheduleFrame(step);

function Particle() {
  this.angle = 0;
  this.vx = 0;
  this.vy = 0;
  this.x = 0;
  this.y = 0;
}

function $(id) {
  return document.getElementById(id);
}

function anglify(rad) {
  return rad % (2 * Math.PI);
}

function calcRate() {
  var time = new Date().getTime();
  var period = time - lastTime;
  lastTime = time;
  var ratio = 0.1;
  // From ms/frame to frame/s.
  var newRate = 1000 / period;
  rate = (1 - ratio) * rate + ratio * newRate;
  $('rate').innerHTML = formatTenths(rate) + " fps";
}

function init() {
  var particleCount = 100;
  for (var p = 0; p < particleCount; p++) {
    var particle = new Particle();
    particle.angle = randBetween(0, 2 * Math.PI);
    particle.x = randBetween(-100, 100);
    particle.y = randBetween(-100, 100);
    particle.vx = 0;
    particle.vy = 0;
    particles.push(particle);
  }
}

function moveWorld() {
  var total = 0;
  var limitSmall = 1e-3;
  var totalSmall = 0;
  var totalRepellant = 0;
  // Calculate and apply forces.
  for (var i = 0; i < particles.length; i++) {
    var p1 = particles[i];
    for (var j = i + 1; j < particles.length; j++) {
      var p2 = particles[j];
      var dx = p2.x - p1.x;
      var dy = p2.y - p1.y;
      if (false) {
        // This seems to slow things down over time. Not average here!
        dx = dx + ((p2.x + p2.vx) - (p2.x + p1.vx)) / 2;
        dy = dy + ((p2.y + p2.vy) - (p2.y + p1.vy)) / 2;
      }
      var r = Math.sqrt(dx * dx + dy * dy);
      if (r < 5.0) {
        r = 5.0;
      }
      var attractive = 1 / (r * r);
      var repellent = -100 / Math.pow(r, 3);
      total++;
      if (attractive < limitSmall && -repellent < limitSmall) {
        totalSmall++;
      } /* else */ {
        applyForce(attractive);
        applyForce(repellent);
      }
      if (-repellent >= limitSmall) {
        // The repellent force is high. They must be close.
        totalRepellant++;
        // Connectors only are active when close.
        for (var ci = 0; ci < 4; ci++) {
          // Evenly spaced at right angles for now.
          var a1 = ci * Math.PI / 2;
          var cdx1 = Math.cos(a1);
          var cdy1 = Math.sin(a1);
          var cx1 = p1.x + cdx1;
          var cy1 = p1.y + cdy1;
          for (var cj = 0; cj < 4; cj++) {
            // Connector for p2.
            var a2 = ci * Math.PI / 2;
            var cdx2 = Math.cos(a2);
            var cdy2 = Math.sin(a2);
            var cx2 = p2.x + cdx2;
            var cy2 = p2.y + cdy2;
            // Relative measures: Alignment, difference, and distance.
            var alignment = Math.abs(cdx1 * cdx2 + cdy1 * cdy2);
            var cdx = cdx2 - cdx1;
            var cdy = cdy2 - cdy1;
            var cr = Math.sqrt(cdx * cdx + cdy * cdy);
            // TODO Use a PD spring controller to snap together.
          }
        }
      }
    }
  }
  // Move particles.
  for (var i = 0; i < particles.length; i++) {
    var particle = particles[i];
    particle.x += particle.vx;
    particle.y += particle.vy;
  }
  // Report small.
  $('small').innerHTML =
    //totalRepellant + " repellant interactions";
    formatTenths(100 * totalRepellant / total) + "% repellant interactions";
    //formatTenths(100 * totalSmall / total) + "% small forces";
  // Functions.
  function applyForce(force) {
    var ax = dx * force;
    var ay = dy * force;
    p1.vx += ax;
    p1.vy += ay;
    p2.vx -= ax;
    p2.vy -= ay;
  }
}

function paintParticle(g, particle) {
  g.save();
  try {
    g.translate(particle.x, particle.y);
    g.rotate(rad2Deg(particle.angle));
    // TODO How to pick proper sizes on these things? What are the units?
    g.fillRect(-3, -1, 6, 2);
    g.fillRect(-1, -3, 2, 6);
  } finally {
    g.restore();
  }
}

function paintWorld() {
  var canvas = $('display');
  if (canvas.height != canvas.clientHeight) {
    canvas.height = canvas.clientHeight;
  }
  if (canvas.width != canvas.clientWidth) {
    canvas.width = canvas.clientWidth;
  }
  var g = canvas.getContext('2d');
  g.clearRect(0, 0, canvas.width, canvas.height);
  g.save();
  try {
    g.translate(canvas.width / 2, canvas.height / 2);
    g.fillStyle = 'blue';
    for (var p = 0; p < particles.length; p++) {
      paintParticle(g, particles[p]);
    }
  } finally {
    g.restore();
  }
}

function rad2Deg(rad) {
  return 180.0 * rad / Math.PI;
}

function randBetween(min, max) {
  return (max - min) * Math.random() + min;
}

// Based on example here:
// https://developer.mozilla.org/en/DOM/window.requestAnimationFrame
var requestAnimationFrame =
  window.requestAnimationFrame || window.mozRequestAnimationFrame ||  
  window.msRequestAnimationFrame || window.webkitRequestAnimationFrame;  
function scheduleFrame(action) {
  if (requestAnimationFrame) {
    requestAnimationFrame(step);
  } else {
    window.setTimeout(step, 20);
  }
}

function step() {
  moveWorld();
  paintWorld();
  calcRate();
  scheduleFrame(step);
}

function formatTenths(number) {
  var tenths = Math.round(10 * number);
  var ones = Math.floor(tenths / 10);
  var tenths = tenths % 10;
  return ones + "." + tenths;
}

};
